﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;
using ICSharpCode.SharpZipLib.Zip;
using NHibernate;
using NHibernate.Linq;
using Orchard;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.Roles.Models;
using OrchardPo.Models;
using OrchardPo.ViewModels;

namespace OrchardPo.Services {
    public interface ILocalizationService : IDependency {
        CultureDetailsViewModel GetCultureDetailsViewModel(string culture);
        CultureGroupDetailsViewModel GetGroupDetailsViewModel(string culture, string group);
        byte[] GetZipBytes(string culture);
        CultureIndexViewModel GetExistingTranslationCultures();
        void UpdateTranslation(int id, string culture, string value);
        void RemoveTranslation(int id, string culture);
        void UnzipPostedFileToDatabase(HttpPostedFileBase file);
        bool IsCultureAllowed(string culture);
        void Cleanup();
    }

    public class LocalizationService : ILocalizationService {
        private readonly ISessionLocator _sessionLocator;
        private readonly IWorkContextAccessor _wca;

        public LocalizationService(ISessionLocator sessionLocator, IWorkContextAccessor wca) {
            _sessionLocator = sessionLocator;
            _wca = wca;
        }

        public CultureDetailsViewModel GetCultureDetailsViewModel(string culture) {
            var model = new CultureDetailsViewModel {Culture = culture};
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                var query = session.CreateQuery(@"
                    select s
                    from OrchardPo.Models.LocalizableStringRecord as s fetch all properties
                    order by s.Path");
                var currentPath = "";
                var group = default(CultureDetailsViewModel.TranslationGroupViewModel);
                foreach (LocalizableStringRecord s in query.Enumerable()) {
                    if (s.Path != currentPath) {
                        group = new CultureDetailsViewModel.TranslationGroupViewModel {
                                                                                          Path = String.Format(s.Path, culture)
                                                                                      };
                        model.Groups.Add(group);
                        currentPath = s.Path;
                    }
                    var translation = s.Translations.FirstOrDefault(t => t.Culture == culture);
                    if (group != null &&
                        ((s.Translations.Count(t => t.Culture.Equals("en-US", StringComparison.OrdinalIgnoreCase)) > 0) ||
                         (translation != null && !String.IsNullOrEmpty(translation.Value)))) {
                        group.Translations.Add(new CultureDetailsViewModel.TranslationViewModel {
                            Context = s.Context,
                            Key = s.StringKey,
                            OriginalString = s.OriginalLanguageString,
                            LocalString = translation != null ? translation.Value : null
                        });
                    }
                }
            }
            return model;
        }

        public CultureGroupDetailsViewModel GetGroupDetailsViewModel(string culture, string group) {
            var model = new CultureGroupDetailsViewModel { Culture = culture };
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                var paths =
                    session.CreateSQLQuery(
                        @"
                    SELECT Localizable.Path,
                        COUNT(COALESCE (Localized.Value, English.Value)) AS TotalCount,
                        COUNT(English.Value) AS EnglishCount,
                        COUNT(COALESCE (Localized.Value, English.Value)) - COUNT(Localized.Value) AS TranslatedCount,
                        COUNT(COALESCE (Localized.Value, English.Value)) - COUNT(English.Value) AS ObsoleteCount
                    FROM OrchardPo_LocalizableStringRecord AS Localizable
                    LEFT OUTER JOIN OrchardPo_TranslationRecord AS Localized
                        ON Localizable.Id = Localized.LocalizableStringRecord_id
                        AND Localized.Culture = :culture
                    LEFT OUTER JOIN OrchardPo_TranslationRecord AS English
                        ON Localizable.Id = English.LocalizableStringRecord_id
                        AND English.Culture = N'en-US'
                    GROUP BY Localizable.Path
                    ORDER BY Localizable.Path")
                    .AddScalar("Path", NHibernateUtil.String)
                    .AddScalar("TotalCount", NHibernateUtil.Int32)
                    .AddScalar("EnglishCount", NHibernateUtil.Int32)
                    .AddScalar("TranslatedCount", NHibernateUtil.Int32)
                    .AddScalar("ObsoleteCount", NHibernateUtil.Int32)
                    .SetParameter("culture", culture);
                model.Groups = paths.List<object[]>()
                    .Select(t => new CultureGroupDetailsViewModel.TranslationGroup {
                        Path = (string)t[0],
                        TotalCount = (int)t[1],
                        EnglishCount = (int)t[2],
                        MissingCount = (int)t[3],
                        ObsoleteCount = (int)t[4],
                        IsComplete = (int)t[3] == 0 && (int)t[4] == 0
                    }).ToList();
                var firstOrDefault = model.Groups.FirstOrDefault();
                if (firstOrDefault != null)
                    model.CurrentGroupPath =
                        String.IsNullOrEmpty(group)
                            ? firstOrDefault.Path
                            : model.Groups.Any(g => g.Path == group)
                                  ? group
                                  : null;

                // Need to go native here because NHib doesn't support multiple join conditions
                var groupTranslations = session.CreateSQLQuery(@"
                    SELECT Localizable.Id, Localizable.Path, Localizable.StringKey,
                           Localizable.Context, Localizable.OriginalLanguageString,
                           Localized.Value as Localized, English.Value as English
                    FROM OrchardPo_LocalizableStringRecord AS Localizable
                    LEFT OUTER JOIN OrchardPo_TranslationRecord AS Localized
                        ON Localizable.Id = Localized.LocalizableStringRecord_id
                        AND Localized.Culture = :culture
                    LEFT OUTER JOIN OrchardPo_TranslationRecord AS English
                        ON Localizable.Id = English.LocalizableStringRecord_id
                        AND English.Culture = N'en-US'
                    WHERE Localizable.Path = :group
                    ORDER BY Localizable.Context + Localizable.StringKey")
                    .AddScalar("Id", NHibernateUtil.Int32)
                    .AddScalar("Path", NHibernateUtil.String)
                    .AddScalar("StringKey", NHibernateUtil.String)
                    .AddScalar("Context", NHibernateUtil.String)
                    .AddScalar("OriginalLanguageString", NHibernateUtil.String)
                    .AddScalar("Localized", NHibernateUtil.String)
                    .AddScalar("English", NHibernateUtil.String)
                    .SetParameter("group", model.CurrentGroupPath)
                    .SetParameter("culture", culture);
                model.CurrentGroupTranslations = groupTranslations.List<object[]>()
                    .Select(t => new CultureGroupDetailsViewModel.TranslationViewModel {
                        Id = (int)t[0],
                        GroupPath = (string)t[1],
                        Key = (string)t[2],
                        Context = (string)t[3],
                        OriginalString = (string)t[4],
                        LocalString = (string)t[5],
                        ExistsInEnglish = !String.IsNullOrWhiteSpace(t[6] as string)
                    })
                    .Where(vm => vm.ExistsInEnglish || !String.IsNullOrWhiteSpace(vm.LocalString))
                    .ToList();
            }
            return model;
        }

        private static void CopyFileToDatabase(ISession session, string path, string content, string culture) {
            string currentContext = null;
            string currentOriginal = null;
            string currentId = null;
            using (var textStream = new StringReader(content)) {
                string line;
                while ((line = textStream.ReadLine()) != null) {
                    if (line.StartsWith("#: ")) {
                        currentContext = line.Substring(3);
                    }
                    if (line.StartsWith("msgctxt ")) {
                        currentContext = line.Substring(8).Trim('"');
                    }
                    else if (line.StartsWith("#| msgid \"")) {
                        currentId = line.Substring(10, line.Length - 11);
                    }
                    else if (line.StartsWith("msgid \"")) {
                        currentOriginal = line.Substring(7, line.Length - 8);
                    }
                    else if (line.StartsWith("msgstr \"")) {
                        var id = currentId;
                        var context = currentContext;
                        SaveStringToDatabase(session, path, context, id, currentOriginal, culture, line);
                    }
                }
            }
            if (culture.Equals("en-us", StringComparison.OrdinalIgnoreCase)) {
                SyncEnUs(session);
            }
        }

        private static void SaveStringToDatabase(ISession session, string path, string context, string id, string original, string culture, string translated) {
            path = path.Replace('\\', '/');
            var lastSlash = path.LastIndexOf('/');
            var slashBeforeThat = path.LastIndexOf('/', lastSlash - 1);
            path = path.Substring(0, slashBeforeThat + 1) + "{0}" + path.Substring(lastSlash);
            var translatedString =
                (from s in session.Linq<LocalizableStringRecord>()
                 where s.StringKey == id
                    && s.Context == context
                 select s).FirstOrDefault();
            if (translatedString == null) {
                translatedString = new LocalizableStringRecord {
                    Path = path,
                    Context = context,
                    StringKey = id,
                    OriginalLanguageString = original
                };
                session.Save(translatedString);
            }
            if (!culture.Equals("en-us", StringComparison.OrdinalIgnoreCase)) {
                var translation =
                    (from t in session.Linq<TranslationRecord>()
                     where t.Culture.Equals(culture)
                           && t.LocalizableStringRecord.Id == translatedString.Id
                     select t).FirstOrDefault() ?? new TranslationRecord {
                         Culture = culture,
                         Value = translated.Substring(8, translated.Length - 9)
                     };
                if (translation.LocalizableStringRecord == null &&
                    !String.IsNullOrWhiteSpace(translation.Value)) {

                    translatedString.AddTranslation(translation);
                    session.SaveOrUpdate(translation);
                }
            }
        }

        public static void SyncEnUs(ISession session) {
            var dropQuery = session.CreateSQLQuery("delete from OrchardPo_TranslationRecord where Culture = N'en-US'");
            dropQuery.ExecuteUpdate();
            var recreateQuery = session.CreateSQLQuery(
                "insert into OrchardPo_TranslationRecord(LocalizableStringRecord_Id, Culture, Value) " +
                "select Id, N'en-US', OriginalLanguageString from OrchardPo_LocalizableStringRecord");
            recreateQuery.ExecuteUpdate();
        }

        public static void RemoveOrphanTranslations(ISession session) {
            var dropQuery = session.CreateSQLQuery("delete from OrchardPo_TranslationRecord where LocalizableStringRecord_Id is null");
            dropQuery.ExecuteUpdate();
        }

        public static void RemoveDuplicateLocalizableStrings(ISession session) {
            var query = session
                .CreateSQLQuery(@"select distinct Path, Context, StringKey from OrchardPo_LocalizableStringRecord")
                .AddScalar("Path", NHibernateUtil.String)
                .AddScalar("Context", NHibernateUtil.String)
                .AddScalar("StringKey", NHibernateUtil.String);
            foreach (var record in query.List<object[]>()) {
                var ids = session.CreateSQLQuery(@"
select Id from OrchardPo_LocalizableStringRecord
where Path=:path and Context=:context
and StringKey=:key")
                    .AddScalar("Id", NHibernateUtil.Int32)
                    .SetParameter("path", (string)record[0])
                    .SetParameter("context", (string)record[1])
                    .SetParameter("key", (string)record[2])
                    .List<int>();
                if (ids.Count <= 1) continue;
                var firstId = ids.First();
                var updateQuery = session
                    .CreateSQLQuery(@"
update OrchardPo_TranslationRecord
set LocalizableStringRecord_Id=:firstId
where LocalizableStringRecord_Id in (" +
                                    String.Join(",", ids) + ")")
                                    .SetParameter("firstId", firstId);
                updateQuery.ExecuteUpdate();
                var deleteQuery = session
                    .CreateSQLQuery(@"
delete from OrchardPo_LocalizableStringRecord
where Path=:path and Context=:context
and StringKey=:key
and not Id=:firstId")
                    .SetParameter("firstId", firstId)
                    .SetParameter("path", (string) record[0])
                    .SetParameter("context", (string) record[1])
                    .SetParameter("key", (string) record[2]);
                deleteQuery.ExecuteUpdate();
            }
        }

        public static void RemoveQuotesFromContext(ISession session) {
            var updateQuery = session
                .CreateSQLQuery(@"
update OrchardPo_LocalizableStringRecord
set Context = substring(Context, 2, len(Context) - 2)
where Context like '""%""'");
            updateQuery.ExecuteUpdate();
        }

        public void Cleanup() {
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                SyncEnUs(session);
                RemoveOrphanTranslations(session);
                RemoveDuplicateLocalizableStrings(session);
            }
        }

        public byte[] GetZipBytes(string culture) {
            var model = GetCultureDetailsViewModel(culture);
            var stream = new MemoryStream();
            var zip = new ZipOutputStream(stream);
            using (var writer = new StreamWriter(zip, Encoding.UTF8)) {
                foreach (var translationGroup in model.Groups) {
                    var file = new ZipEntry(translationGroup.Path) {DateTime = DateTime.Now};
                    zip.PutNextEntry(file);
                    writer.WriteLine(@"# Orchard resource strings - {0}
# Copyright (c) 2010 Outercurve Foundation
# All rights reserved
# This file is distributed under the BSD license
", culture);
                    foreach (var translation in translationGroup.Translations) {
                        writer.WriteLine("#: " + translation.Context);
                        writer.WriteLine("#| msgid \"" + translation.Key + "\"");
                        writer.WriteLine("msgctxt \"" + translation.Context + "\"");
                        writer.WriteLine("msgid \"" + translation.OriginalString + "\"");
                        writer.WriteLine("msgstr \"" + translation.LocalString + "\"");
                        writer.WriteLine();
                    }
                    writer.Flush();
                }
            }
            zip.IsStreamOwner = false;
            zip.Close();
            return stream.ToArray();
        }

        public CultureIndexViewModel GetExistingTranslationCultures() {
            var model = new CultureIndexViewModel();
            using (var session = _sessionLocator.For(typeof(TranslationRecord))) {
                var cultures =
                    (from t in session.Linq<TranslationRecord>()
                     group t by t.Culture
                     into c
                     select new {
                                    c.First().Culture,
                                    Count = c.Count()
                                }
                    ).ToList();
                foreach (var culture in cultures) {
                    model.TranslationStates.Add(
                        culture.Culture,
                        new CultureIndexViewModel.CultureTranslationState
                        {NumberOfTranslatedStrings = culture.Count});
                }
                model.NumberOfStringsInDefaultCulture =
                    (from t in session.Linq<TranslationRecord>()
                     where t.Culture == "en-US"
                     select t).Count();
            }
            return model;
        }

        public void UpdateTranslation(int id, string culture, string value) {
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                var localizable = session.Get<LocalizableStringRecord>(id);
                var translation = localizable.Translations
                    .FirstOrDefault(t => t.Culture == culture);
                if (translation == null) {
                    translation =
                        new TranslationRecord {
                                                  Culture = culture,
                                                  Value = value,
                                              };
                    localizable.AddTranslation(translation);
                    session.Persist(translation);
                    session.Update(localizable);
                }
                else {
                    translation.Value = value;
                    session.SaveOrUpdate(translation);
                }
            }
        }

        public void RemoveTranslation(int id, string culture) {
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                var translation = session.Get<LocalizableStringRecord>(id)
                    .Translations.FirstOrDefault(t => t.Culture == culture);
                if (translation != null) {
                    translation.LocalizableStringRecord.Translations.Remove(translation);
                    session.Delete("Translation", translation);
                }
            }
        }

        public void UnzipPostedFileToDatabase(HttpPostedFileBase file) {
            using (var session = _sessionLocator.For(typeof(LocalizableStringRecord))) {
                var zip = new ZipInputStream(file.InputStream);
                ZipEntry zipEntry;
                while ((zipEntry = zip.GetNextEntry()) != null) {
                    if (zipEntry.IsFile) {
                        var entrySize = (int) zipEntry.Size;
                        // Yeah yeah, but only a handful of people have upload rights here for the moment.
                        var entryBytes = new byte[entrySize];
                        zip.Read(entryBytes, 0, entrySize);
                        var content = entryBytes.ToStringUsingEncoding();
                        var cultureName = Path.GetFileName(Path.GetDirectoryName(zipEntry.Name));
                        var culture = cultureName;
                        CopyFileToDatabase(session, zipEntry.Name, content, culture);
                    }
                }
            }
        }

        public bool IsCultureAllowed(string culture) {
            var ctx = _wca.GetContext();
            var rolesPart = ctx.CurrentUser.As<UserRolesPart>();
            return (rolesPart != null && rolesPart.Roles.Contains(culture));
        }
    }
}